#define ANDROID_USE_FF

using System;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.UI;

// Usage
// framePredictionObject.SetActive(true/false);  // will use 720p to render
// framePredictionObject.SetFfEnable(true/false);  // will enable FF

public class FramePredictionComponent : MonoBehaviour
{
    [SerializeField]
    Camera m_camera;

    [SerializeField]
    Text m_tip_text;

    private bool m_enable_render_resolution = true;  // use low resolution when active

    private bool m_enable_ff = true;
    private bool m_is_init = false;  // init instance and textures
    private bool m_init_failed = false;  // Init fail, not to retry
    private bool m_active = false;

    private IFFInstance m_instance = null;
    private RenderTexture m_predicted_tex = null;
    private RenderTexture m_camera_tex = null;
    private RenderTexture m_prev_camera_tex = null;

    private int m_render_width = -1;
    private int m_render_height = -1;
    private int m_display_width = -1;
    private int m_display_height = -1;
    private bool m_extrapolation = true;

    private string TAG = "FramePrediction: ";
    private string m_graphic_api = "NA";

    private bool m_is_render = false;

    void Start()
    {
        if (m_camera == null) {
            Debug.Log(TAG + "camera is NULL");
            return;
        }
    }

    private void UpdateText()
    {
        if (m_tip_text != null)
        {
            if (m_enable_ff)
            {
                if (m_active)
                {
                    m_tip_text.text = m_graphic_api + " " + (m_init_failed ? "FAIL " : "ON ") + m_render_width + "x" + m_render_height;
                }
                else
                {
                    m_tip_text.text = m_graphic_api + " " + (m_init_failed ? "FAIL " : "OF ") + m_render_width + "x" + m_render_height;
                }
            }
            else
            {
                m_tip_text.text = m_graphic_api + " " + (m_init_failed ? "FAIL " : "OFF ") + m_render_width + "x" + m_render_height;
            }
        }
    }

    public void SetFfEnable(bool enableFf)
    {
        m_enable_ff = enableFf;
        UpdateText();
    }

    private void CreateTexture(ref RenderTexture tex)
    {
        if (tex == null)
        {
            tex = new RenderTexture(m_render_width, m_render_height, 24, RenderTextureFormat.ARGB32);
            tex.Release();
            tex.width = m_render_width;
            tex.height = m_render_height;
            tex.depth = 24;
            tex.format = RenderTextureFormat.ARGB32;
            tex.Create();
        }
    }

    private void ResetRenderResource()
    {
        if (Screen.width != m_display_width || Screen.height != m_display_height)
        {
            m_display_width = Screen.width;
            m_display_height = Screen.height;
            // choose min(width, height) to 720
            if (m_display_width < m_display_height)
            {
                m_render_width = 720;
                m_render_height = (int)(m_display_height * m_render_width / m_display_width / 4) * 4;
            }
            else
            {
                m_render_height = 720;
                m_render_width = (int)(m_display_width * m_render_height / m_display_height / 4) * 4;
            }
            Debug.Log(TAG + "Resolution render(" + m_render_width + "x" + m_render_height + ") display(" + m_display_width + "x" + m_display_height + ")");
        }
        if (m_camera_tex == null || m_camera_tex.width != m_render_width || m_camera_tex.height != m_render_height)
        {
            CreateTexture(ref m_camera_tex);
            Debug.Log(TAG + "CreateTexture camera");
        }
        if (m_predicted_tex == null || m_predicted_tex.width != m_render_width || m_predicted_tex.height != m_render_height)
        {
            CreateTexture(ref m_predicted_tex);
            Debug.Log(TAG + "CreateTexture predicted");
        }
        if (m_prev_camera_tex == null || m_prev_camera_tex.width != m_render_width || m_prev_camera_tex.height != m_render_height)
        {
            CreateTexture(ref m_prev_camera_tex);
            Debug.Log(TAG + "CreateTexture prev camera");
        }
    }

    private void InitFramePrediction()
    {
        if (m_is_init)
        {
            return;
        }
        if (m_init_failed)
        {
            return;
        }
        if (SystemInfo.graphicsDeviceType == GraphicsDeviceType.OpenGLES3)
        {
            m_instance = new FFInstanceGL();
            Debug.Log(TAG + "Plugin support " + SystemInfo.graphicsDeviceType + " API");
            m_graphic_api = "GL";
        }
        else if (SystemInfo.graphicsDeviceType == GraphicsDeviceType.Vulkan)
        {
            m_instance = new FFInstanceVK();
            Debug.Log(TAG + "Plugin support " + SystemInfo.graphicsDeviceType + " API");
            m_graphic_api = "VK";
        }
        else
        {
            Debug.Log(TAG + "Plugin doesnt support " + SystemInfo.graphicsDeviceType + " API");
            m_graphic_api = "NA";
            m_init_failed = true;
            return;
        }

        FFPredictionAlgorithm algorithm = FFPredictionAlgorithm.FF_PREDICTION_ALGORITHM_GRIDWARP;
        if (SystemInfo.graphicsDeviceType == GraphicsDeviceType.OpenGLES3)
        {
            algorithm = FFPredictionAlgorithm.FF_PREDICTION_ALGORITHM_HYDRA;
            m_extrapolation = false;
        }
        if (!m_instance.Init(m_render_width, m_render_height, algorithm, m_extrapolation))
        {
            Debug.Log(TAG + "Cant Init plugin.");
            m_init_failed = true;
            return;
        }
        if (algorithm == FFPredictionAlgorithm.FF_PREDICTION_ALGORITHM_GRIDWARP)
        {
            FFStencilValueSemantic[] remap = new FFStencilValueSemantic[256];
            remap[0] = FFStencilValueSemantic.FF_STENCIL_VALUE_SEMANTIC_STATIC_OBJECT;  // stencil 0 is static object
            for (int i = 1; i < 256; i++)
            {
                remap[i] = FFStencilValueSemantic.FF_STENCIL_VALUE_SEMANTIC_STATIC_OBJECT;
                // remap[i] = FFStencilValueSemantic.FF_STENCIL_VALUE_SEMANTIC_GENERIC_DYNAMIC_OBJECT;
            }
            m_instance.SetStencilRemap(remap);
        }

        m_is_init = true;
    }

    private void ActivateFramePrediction()
    {
        if (!m_instance.Activate())
        {
            Debug.Log(TAG + "Plugin activation failed.");
            m_active = false;
        }
        else
        {
            Debug.Log(TAG + "Plugin activation success.");
            m_active = true;
        }
    }

    private void DeactivateFramePrediction()
    {
        if (m_instance == null)
        {
            Debug.Log(TAG + "Plugin deactivation failed as null instance.");
            m_active = false;
            return;
        }
        m_instance.Deactivate();
        m_active = false;
        Debug.Log(TAG + "Plugin deactivation success.");
    }

    private bool IsActive()
    {
        return m_instance.IsActive();
    }

    private void OnEnable()
    {
        Debug.Log(TAG + "OnEnable");
        m_enable_render_resolution = true;
        ResetRenderResource();  // camera tex
        {
            m_camera.targetTexture = m_camera_tex;
            m_camera.enabled = false;  // First disable it
            m_camera.depthTextureMode = DepthTextureMode.Depth;
        }
        InitFramePrediction();
        ActivateFramePrediction();
        UpdateText();
    }

    private void OnDisable()
    {
        Debug.Log(TAG + "OnDisable");
        m_enable_render_resolution = false;
        {
            m_camera.targetTexture = null;
            m_camera.enabled = true;
            m_camera.depthTextureMode = DepthTextureMode.None;
        }
        DeactivateFramePrediction();
        UpdateText();
    }

    void Stop()
    {
        Debug.Log(TAG + "Stop");
        DeactivateFramePrediction();
        if (m_predicted_tex != null)
        {
            m_predicted_tex.Release();
            m_predicted_tex = null;
        }
        if (m_prev_camera_tex != null)
        {
            m_prev_camera_tex.Release();
            m_prev_camera_tex = null;
        }
    }

    private void LateUpdate()
    {
        if (!m_enable_render_resolution)
        {
            return;
        }

        if (m_init_failed)
        {
            return;
        }

        if (!m_enable_ff)
        {
            m_camera.Render();  // Maybe render more times to add origin render load
            Blit(m_camera_tex);
            return;
        }

        if (m_instance == null)
        {
            return;
        }

        if(!IsActive())
        {
            if (!m_active)
            {
                // manally to call
                Debug.Log(TAG + "LateUpdate call activate");
                ActivateFramePrediction();
            }
        }

        if (!m_active)
        {
            m_camera.Render();  // Maybe render more times to add origin render load
            Blit(m_camera_tex);
            return;
        }

        m_is_render = !m_is_render;
        if (m_extrapolation)
        {
            if (m_is_render)
            {
                m_camera.Render();  // render N
                if (true)
                {
                    Matrix4x4 view_proj = m_camera.projectionMatrix * m_camera.worldToCameraMatrix;
                    IntPtr color = m_camera_tex.GetNativeTexturePtr();
                    IntPtr depth = m_camera_tex.GetNativeDepthBufferPtr();
                    m_instance.Feed(view_proj, color, depth);
                }
                Blit(m_camera_tex);  // show N
            }
            else
            {
                IntPtr predict = m_predicted_tex.GetNativeTexturePtr();
                m_instance.Predict(predict);  // predict N+0.5
                Blit(m_predicted_tex);  // show N+0.5
            }
        }
        else
        {
            if (m_is_render)
            {
                m_camera.Render();  // render N+1
                if (true)
                {
                    Matrix4x4 view_proj = m_camera.projectionMatrix * m_camera.worldToCameraMatrix;
                    IntPtr color = m_camera_tex.GetNativeTexturePtr();
                    IntPtr depth = m_camera_tex.GetNativeDepthBufferPtr();
                    m_instance.Feed(view_proj, color, depth);
                }
                Blit(m_prev_camera_tex);  // show N
                Blit(m_camera_tex, m_prev_camera_tex); // Copy N+1 to N

            }
            else
            {
                Debug.Log(TAG + "LateUpdate 11");
                IntPtr predict = m_predicted_tex.GetNativeTexturePtr();
                m_instance.Predict(predict);  // predict N+0.5
                Blit(m_predicted_tex);  // show N+0.5
            }
        }
    }

    void Blit(RenderTexture renderTexture, RenderTexture target = null)
    {
        Graphics.Blit(renderTexture, target);
    }
}
